package com.yzb.helper;

import com.yzb.config.FtpConfig;
import com.yzb.exception.FtpException;
import com.yzb.ftp.FtpProgressListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.ClassPathResource;

import java.io.*;
import java.util.Properties;

/**
 * FptClient帮助类.
 * <p>
 * 可以
 * <ol>
 * <li>连接到服务器</li>
 * </ol>
 *
 * @author huang yu zhi
 * @version 1.0.0
 * @date 2019-01-25 11:31
 */
@Slf4j
public final class FtpClientHelper {
    /**
     * ftpConfig配置文件的名称.
     */
    private static final String FTP_CONFIG_NAME = "ftp.properties";

    /**
     * ftpClient配置.
     */
    public static final FtpConfig FTP_CONFIG;

    /**
     * ftp根目录.
     **/
    private static final String FTP_ROOT_PATH = "/";

    static {
        Properties properties = new Properties();
        try {
            log.debug("从类路径读取ftp配置文件{}", FTP_CONFIG_NAME);
            InputStream inputStream = new ClassPathResource(FTP_CONFIG_NAME).getInputStream();
            properties.load(inputStream);
        } catch (IOException e) {
            throw new IllegalArgumentException("从类路径上找不到配置文件" + FTP_CONFIG_NAME);
        }
        Iterable<ConfigurationPropertySource> propertySources = ConfigurationPropertySources.from(new PropertiesPropertySource("ftpClient", properties));
        FTP_CONFIG = new Binder(propertySources).bind("ftp", FtpConfig.class).get();

    }

    /**
     * 创建ftpClient对象.
     *
     * @param host           远程主机地址
     * @param userName       用户名
     * @param userPwd        密码
     * @param port           端口
     * @param connectTimeout 连接超时时间
     * @return {@link FTPClient}
     * @throws FtpException 创建对象的过程中出现异常
     */
    public static FTPClient build(String host, String userName, String userPwd, int port, int connectTimeout) throws FtpException {
        FTPClient ftpClient = new FTPClient();
        ftpClient.setConnectTimeout(connectTimeout);
        String localCharset = FTP_CONFIG.getLocalCharset();
        try {
            log.debug("尝试连接到服务器 host={},userName={},userPwd={},port={}", host, userName, userPwd, port);
            ftpClient.connect(host, port);
            if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) {
                localCharset = "utf-8";
            }
            ftpClient.login(userName, userPwd);
        } catch (IOException e) {
            throw new FtpException(String.format("连接到服务器 host=%s,userName=%s,userPwd=%s,port=%d 失败", host, userName, userPwd, port), e);
        }
        log.debug("成功连接服务器");
        ftpClient.setControlEncoding(localCharset);
        try {
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            int replyCode = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(replyCode)) {
                ftpClient.disconnect();
            }
            ftpClient.setControlEncoding("utf-8");
            ftpClient.setBufferSize(FTP_CONFIG.getBufferSize());
        } catch (IOException e) {
            throw new FtpException(String.format("服务器 host=%s 响应出现异常", host), e);
        }
        return ftpClient;
    }

    /**
     * 创建ftpClient对象.
     *
     * @param host     远程主机地址
     * @param userName 用户名
     * @param userPwd  密码
     * @return {@link FTPClient}
     * @throws FtpException 创建对象的过程中出现异常
     */
    public static FTPClient build(String host, String userName, String userPwd) throws FtpException {
        return build(host, userName, userPwd, FTP_CONFIG.getPort(), FTP_CONFIG.getConnectTimeout());
    }

    /**
     * 从ftp服务器下载文件到本地目录.
     *
     * @param ftpClient  FtpClient对象
     * @param remoteFile 远程文件，必须存在
     * @param localDir   本地目录
     * @param overwrite  如果本地目录已存在同样的文件名，是否覆盖
     * @param listener 进度监听
     * @throws FtpException 下载过程中如果出现任何错误时抛出的异常
     * @see FTPClient#retrieveFile(String, java.io.OutputStream)
     * @see FTPClient#retrieveFileStream(String)
     */
    public static void downloadFileToLocalDir(FTPClient ftpClient, String remoteFile, String localDir, boolean overwrite, FtpProgressListener listener) throws FtpException {
        OutputStream outputStream = null;
        InputStream inputStream = null;
        try {
            //本地目录不存在则新建
            File localFolder = FileUtils.getFile(localDir);
            if (localFolder.isFile()) {
                throw new FtpException("[" + localDir + "]不是一个目录");
            }
            if (!localFolder.exists()) {
                localFolder.mkdirs();
            }
            checkClient(ftpClient);
            if (ftpClient.changeWorkingDirectory(remoteFile)) {
                throw new FtpException("[" + remoteFile + "]是目录");
            }
            FTPFile[] ftpFiles = ftpClient.listFiles(remoteFile);
            if (ftpFiles == null || ftpFiles.length == 0) {
                throw new FtpException("[" + remoteFile + "]不存在");
            }
            ftpClient.changeWorkingDirectory(FTP_ROOT_PATH);
            ftpClient.enterLocalPassiveMode();
            inputStream = ftpClient.retrieveFileStream(remoteFile);
            //把远程文件的文件名作为本地文件的文件名
//            String remoteName = remoteFile.substring(remoteFile.lastIndexOf("/") + 1);
            String remoteName = untranslateFtpFileName(ftpFiles[0].getName());
            File localFile = new File(localDir, remoteName);
            if (localFile.exists()) {
                if (!overwrite) {
                    log.info(String.format("本地目录[%s]内已存在[%s]文件", localDir, remoteName));
                    return;
                }
                FileUtils.deleteQuietly(localFile);
                log.info(String.format("删除本地目录[%s]内的[%s]文件", localDir, remoteName));
            }
            outputStream = new FileOutputStream(localFile);
            byte[] buffer = new byte[FTP_CONFIG.getBufferSize()];
            int len, flushSize = FTP_CONFIG.getFlushSize();
            long totalTransferred = ftpFiles[0].getSize(), totalBytesTransferred = 0;
            while ((len = inputStream.read(buffer)) != -1) {
                totalBytesTransferred += len;
                outputStream.write(buffer, 0, len);
                listener.bytesTransferred(totalBytesTransferred, len, totalTransferred);
                if (totalBytesTransferred % flushSize == 0) {
                    outputStream.flush();
                    log.debug("已传输字节{},达到flushSize={}的{}倍,调用flush", totalBytesTransferred, flushSize, totalBytesTransferred / flushSize);
                }
            }
            outputStream.flush();
            outputStream.close();
            //文件名需要转换,避免中文乱码
            log.debug("下载远程文件[" + translateFtpFileName(remoteFile) + "]到本地目录[" + localDir + "]成功,新的文件名为[" + remoteName + "]");
        } catch (IOException e) {
            throw new FtpException(String.format("下载远程文件%s出错", remoteFile), e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
                ftpClient.logout();
                ftpClient.disconnect();
            } catch (IOException e) {
                // ignore
            }
        }
    }

    /**
     * 编码ftp文件名，编码中文乱码.
     *
     * @param fileName 文件名
     * @return 编码后的文件名
     */
    private static String translateFtpFileName(String fileName) {
        try {
            return new String(fileName.getBytes(FTP_CONFIG.getLocalCharset()), FTP_CONFIG.getServerCharset());
        } catch (UnsupportedEncodingException e) {
            //ignore
        }
        return "";
    }

    /**
     * 解码ftp文件名，否则中文乱码的文件名不能正确显示.
     *
     * @param fileName 文件名
     * @return 解码后的文件名
     */
    private static String untranslateFtpFileName(String fileName) {
        try {
            return new String(fileName.getBytes(FTP_CONFIG.getServerCharset()), FTP_CONFIG.getLocalCharset());
        } catch (UnsupportedEncodingException e) {
            //ignore
        }
        return "";
    }

    /**
     * 检查ftpClient是否可用.
     *
     * @param ftpClient ftpClietn对象
     * @throws FtpException 出现异常时抛出
     */
    private static void checkClient(FTPClient ftpClient) throws FtpException {
        if (!ftpClient.isConnected()) {
            throw new FtpException("ftpClient 已断开连接");
        }
    }

}
